/*
 * Copyright 2007 Michał Baliński
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package pl.balon.gwt.diagrams.client.connection.calculator;

import java.util.LinkedList;
import java.util.List;

import pl.balon.gwt.diagrams.client.connection.data.ConnectionData;
import pl.balon.gwt.diagrams.client.connection.data.Point;
import pl.balon.gwt.diagrams.client.connector.Connector;
import pl.balon.gwt.diagrams.client.connector.Direction;

import com.google.gwt.core.client.GWT;

/**
 * Full rectilinear connections data calculator.
 *
 * @author Alex Pinheiro das Graças (alex.pgracas@gmail.com)
 */
public class FullRectilinearTwoEndedCalculator implements ConnectionDataCalculator {

	public enum Alingnment{Horizontal,Vertical};

	/**
	 * Calculates the new points when some point is moved.
	 * @param connectors The list of the connectors ( source and target) of the connection.
	 * @param data The points that the connection pass
	 * @param PointChanged The point that was moved.
	 * @return The new points
	 */
	public ConnectionData calculateConnectionData(List<Connector> connectors, ConnectionData data, int changed,int pleft, int ptop) {

		LinkedList<Alingnment> alingnments = getAlingnmentVector(data);

		GWT.log("Point changed:" + changed, null);
		printState(data, alingnments);

		LinkedList<Point> points = data.getPoints();
		alingnments=null;
		if (changed == 2 || changed < 0){
			calculateHead(connectors, data, points);
		}
		if(changed == points.size()-3 || changed < 0) {
			calculateTail(connectors, data, points);
		}
		alingnments = getAlingnmentVector(data);
		GWT.log("Fix Head and Tail", null);
		printState(data, alingnments);
		int previousSize  = data.getPoints().size();
		for(int i=2; i< alingnments.size()-1;i++) {
			if(alingnments.get(i)==null) {
				alingnments = getAlingnmentVector(data);
				GWT.log("Linking Point is needed: "+(i+1), null);printState(data, alingnments);
				if(!this.alignIfIsPossiblePreviousPoint(i+1, alingnments, data)) {

					GWT.log("Creating new Point: "+(i+1), null);printState(data, alingnments);

					linkPoints(i+1, alingnments, data);

					GWT.log("Point created: ", null);printState(data, alingnments);
				}
			}
		}
		alingnments = getAlingnmentVector(data);
		GWT.log("After link all points", null);
		printState(data, alingnments);
		int i = 2;
		while( i < alingnments.size()-1) {
			if(alingnments.get(i)==alingnments.get(i-1)) {
				deleteUnecessaryPoints(i, alingnments, data);
			}else {
				i++;
			}
		}
		if(changed==-1 && previousSize< points.size()){
			while(previousSize-->0) {
				removePointAndNearestNeighbour(connectors,points.size()/2, data);
			}
			calculateConnectionData(connectors, data,-2,0,0);
		}
		GWT.log("Finished!", null);
		printState(data, alingnments);

		return data;
	}

	private void printState(ConnectionData data,
			LinkedList<Alingnment> alingnments) {
		GWT.log( imprimirPontos(data.getPoints()), null);
		GWT.log(imprimirDirecao(alingnments), null);
	}

	public void removePointAndNearestNeighbour(List<Connector> connectors,int indexPoint, ConnectionData data) {
		if(data.getPoints().size()>6) {
			Point p1 = data.getPoints().get(indexPoint);
			Point pNext = data.getPoints().get(indexPoint+1);
			Point pPrev = data.getPoints().get(indexPoint-1);
			double distanceNext = p1.distance(pNext);
			double distancePrev = p1.distance(pPrev);
			if(distanceNext<distancePrev) {
				data.getPoints().remove(indexPoint+1);
				data.getPoints().remove(indexPoint);
			}else {
				data.getPoints().remove(indexPoint);
				data.getPoints().remove(indexPoint-1);
			}
		}
	}
	private void  calculateHead(List<Connector> connectors, ConnectionData data,	LinkedList<Point> points) {
		Point p1 = data.getPoints().get(2);
		LinkedList<Alingnment> alingnments;
		Connector c= connectors.get(0);
		Point[] borderPoints = createPointConnection(c, computeDirection(p1, c));
		points.removeFirst();
		points.removeFirst();
		points.addFirst(borderPoints[1]);
		points.addFirst(borderPoints[0]);
		alingnments = getAlingnmentVector(data);
		createConnectorConnection(0,1,2,alingnments,data);
	}

	private void calculateTail(List<Connector> connectors, ConnectionData data,LinkedList<Point> points) {
		int thirdLast = data.getPoints().size()-3;
		Point p1 = data.getPoints().get(thirdLast);
		Connector c= connectors.get(1);
		Point[] borderPoints = createPointConnection(c, computeDirection(p1, c));
		points.removeLast();
		points.removeLast();
		points.add(borderPoints[1]);
		points.add(borderPoints[0]);
		LinkedList<Alingnment>  alingnments = getAlingnmentVector(data);
		createConnectorConnection(thirdLast+2,thirdLast+1,thirdLast,alingnments,data);
	}


	private static final int WIDGET_SIZE=30;

	/**
	 *
	 * @param p1Connector Connector Point
	 * @param p2PrevPoint Second Point
	 * @param p3ChangedPoint Third Point
	 * @param alingnments The vector with all alingments among points.
	 * @param data The Connection data.
	 */
	protected void createConnectorConnection(int p1Connector, int p2PrevPoint, int p3ChangedPoint,LinkedList<Alingnment> alingnments, ConnectionData data) {
		Point point = data.getPoints().get(p3ChangedPoint);
		Point prevPoint = data.getPoints().get(p2PrevPoint);
		//Point connectorPoint = data.getPoints().get(p1Connector);
		Point newPoint = null;
		assert (p3ChangedPoint ==  2 || p3ChangedPoint == data.getPoints().size()-3);
		Alingnment connectorAlingnment =  alingnments.get(Math.min(p1Connector, p2PrevPoint));
		Alingnment appendAlingnment = alingnments.get(Math.min(p2PrevPoint, p3ChangedPoint));
		if(connectorAlingnment!= null && appendAlingnment!=null) {
			//If everything is aligned nothing to do.
			//Could treat alingnment errors like Go and Back
			return;
		}
		if(connectorAlingnment==Alingnment.Vertical && 	appendAlingnment==null) {
//			Direction one = this.getVectorDirection(data.getPoints().get(p2PrevPoint), data.getPoints().get(p1Connector));
			if(Math.abs(point.left - prevPoint.left)> WIDGET_SIZE) {
				newPoint = new Point(point.left,prevPoint.top);
			}else {
				int add = point.top>prevPoint.top?WIDGET_SIZE:-WIDGET_SIZE;
				newPoint = new Point(point.left+add,prevPoint.top);
			}
		}else if(connectorAlingnment==Alingnment.Horizontal && appendAlingnment==null) {
//			Direction d = this.getVectorDirection(data.getPoints().get(p2PrevPoint), data.getPoints().get(p2PrevPoint-1));
			if(Math.abs(point.left - prevPoint.left) > WIDGET_SIZE) {
				newPoint = new Point(prevPoint.left,point.top);
			}else {
				int add = point.top>prevPoint.top?WIDGET_SIZE:-WIDGET_SIZE;
				newPoint = new Point(prevPoint.left,point.top+add);
			}
		}
		data.getPoints().add(Math.max(p2PrevPoint, p3ChangedPoint), newPoint);
	}

	public String imprimirPontos(List<Point> points) {
		StringBuilder message = new StringBuilder();
		int i =0 ;
		for(Point p1: points) {
			message.append("|| "+ (i++) +" - Left: " +p1.left + " Top: " + p1.top + " \n");
		}
		return message.toString();
	}

	public String imprimirDirecao(List<Alingnment> directions) {
		StringBuilder message = new StringBuilder();
		int i =0 ;
		for(Alingnment p1: directions) {
			message.append(" || "+ (i++) +": "+  p1 );
		}
		return message.toString();
	}

	/**
	 * Delete a point if it is not necessary.
	 * @param p2 The point index
	 * @param direction The vector with the direction of all lines
	 * @param data
	 */
	protected int deleteUnecessaryPoints(int p2,LinkedList<Alingnment> alingnments, ConnectionData data) {
		int p1 = p2-1;
		if(alingnments.get(p1) != null) {//If it is alingned it is not necessary to add points to link them.
			//If the three points are in line we can remove one of them.
			if(alingnments.get(p2) != null && alingnments.get(p2) == alingnments.get(p1)){
				//deleting the point and computing the new direction
				data.getPoints().remove(p2);
				alingnments.remove(p2);
				return -1;
			}
		}
		return 0 ;
	}
	/**
	 * Look for points that can be deleted, and case creates points
	 * to link p1 and p2.
	 * @param p1
	 * @param p2
	 * @param p3
	 * @param data
	 */
	protected int linkPoints(int p2,LinkedList<Alingnment> direction, ConnectionData data) {
		int p1 = p2-1;
		int p3 = p2+1;
		Point p = null;
		int result = 0;
		//result = deleteUnecessaryPoints(p2, direction, data);
		if(direction.get(p1) == null){
			Point prevPoint = data.getPoints().get(p1);
			Point point = data.getPoints().get(p2);
			Point nextPoint = data.getPoints().get(p3);
			if(direction.get(p2) != null) {
				if(direction.get(p2)==Alingnment.Horizontal) {
					p = new Point(prevPoint.left,point.top);
				}else{
					p = new Point(point.left,prevPoint.top);
				}
			}else if((p1-1>=0) && (direction.get(p1-1) != null)) {
				if(direction.get(p1-1)==Alingnment.Vertical) {
					p = new Point(prevPoint.left,point.top);
				}else{
					p = new Point(point.left,prevPoint.top);
				}
			}else {
				int newLeft,newTop;
				if(Math.abs(point.left-prevPoint.left) < Math.abs(point.left-nextPoint.left)) {
					newLeft = point.left;
					newTop = prevPoint.top;
				}else {
					newLeft = prevPoint.left;
					newTop = point.top;
				}
				p = new Point(newLeft,newTop);
			}
			data.getPoints().add(p2,p);
			direction.remove(p1);
			direction.add(p1,findPointsAlingnment(data.getPoints().get(p1), data.getPoints().get(p2)));
			direction.add(p2,findPointsAlingnment(data.getPoints().get(p2), data.getPoints().get(p3)));
			return result++;
		}
		return result;
	}

	protected boolean alignIfIsPossiblePreviousPoint(int p,LinkedList<Alingnment> alingnments, ConnectionData data) {
		Direction direction = getVectorDirection(data.getPoints().get(p-2), data.getPoints().get(p-1));
		Point prevPoint = data.getPoints().get(p-1);
		Point point = data.getPoints().get(p);
		if(direction==Direction.LEFT && prevPoint.left<point.left){
			prevPoint.left=point.left;
			alingnments.set(p-1,Alingnment.Vertical);
			return true;
		}else if(direction==Direction.RIGHT && prevPoint.left>point.left){
			prevPoint.left=point.left;
			alingnments.set(p-1,Alingnment.Vertical);
			return true;
		}else if(direction==Direction.UP && prevPoint.top>point.top){
			prevPoint.top=point.top;
			alingnments.set(p-1,Alingnment.Horizontal);
			return true;
		}else if(direction==Direction.DOWN && prevPoint.top<point.top){
			prevPoint.top=point.top;
			alingnments.set(p-1,Alingnment.Horizontal);
			return true;
		}
		return false;
	}

	/**
	 * Check the points looking for some points that can be deleted.
	 *
	 * @param data
	 */
	protected LinkedList<Alingnment> getAlingnmentVector(ConnectionData data) {
		List<Point> points = data.getPoints();
		LinkedList<Alingnment> direction = new LinkedList<Alingnment>();
		for(int i=0; i< data.getPoints().size()-1;i++) {
			direction.add(findPointsAlingnment(points.get(i), points.get(i+1)));
		}
		return direction;
	}

	protected final Direction getVectorDirection(Point p1, Point p2) {
		if(p1.left == p2.left) {
			if(p1.top>p2.top) {
				return Direction.DOWN;
			}
			return Direction.UP;
		}else if(p1.top == p2.top) {
			if(p1.left>p2.left)
				return Direction.RIGHT;
			else
				return Direction.LEFT;
		}
		return null;
	}

	protected final Alingnment findPointsAlingnment(Point p1, Point p2) {
		if(p1.left == p2.left) {
			return Alingnment.Vertical;
		}else if(p1.top == p2.top) {
			return Alingnment.Horizontal;
		}
		return null;
	}

	Point[] createPointConnection(Connector c1, Direction direction) {
		Point p[] = new Point[2];
		p[0] = c1.pointOnBorder(direction);
		p[1] = p[0].move(direction, 15); // TODO paremetrize it
		return p;
	}

	/**
	 * @see pl.balon.gwt.diagrams.client.connection.calculator.ConnectionDataCalculator#calculateConnectionData(java.util.List)
	 */
	public ConnectionData createConnectionData(List<Connector> connectors) {
		if( connectors.size() != 2 ){
			throw new IllegalArgumentException("Unsupported connectors count");
		}

		// Getting the connectors
		Connector c1 = connectors.get(0);
		Connector c2 = connectors.get(1);

		ConnectionData data = new ConnectionData();


		Direction[] directions = computeDirections(c1, c2);

		Point s1 = c1.pointOnBorder(directions[0]);
		Point s11 = s1.move(directions[0], 10); // TODO paremetrize it

		Point s2 = c2.pointOnBorder(directions[1]);
		Point s21 = s2.move(directions[1], 10); // TODO paremetrize it

		data.getPoints().add(s1);
		data.getPoints().add(s11);
		data.getPoints().add(new Point((s11.left+s21.left)/2 , (s11.top+s21.top)/2));
		data.getPoints().add(s21);
		data.getPoints().add(s2);

		return this.calculateConnectionData(connectors, data, -1, 0, 0);
	}


	/**
	 * TODO externalize to some super class
	 */
	private Direction[] computeDirections(Connector c1, Connector c2) {
		Direction[] d1 = c1.getDirections();
		Direction[] d2 = c2.getDirections();

		// Getting a direction to start.
		Direction[] bestDirections = new Direction[2];

		//Avoiding problems when you have a recursive link.
		//By - Alex
		if(c1==c2) {
			bestDirections[0] = d1[0];
			bestDirections[1] = d1[1];
			return bestDirections;
		}

		// Calculate the best points with less distance.
		double best = Double.MAX_VALUE;
		for (int i = 0; i < d1.length; i++) {
			for (int j = 0; j < d2.length; j++) {
				double actual = c1.getConnectionPoint(d1[i]).distance(c2.getConnectionPoint(d2[j]));
				if( actual < best ){
					best = actual;
					bestDirections[0] = d1[i];
					bestDirections[1] = d2[j];
				}
			}
		}

		return bestDirections;
	}

	/**
	 * This method capture the best place to a point link with a connector.
	 * It is used when you are moving points near the border of a connector.
	 * @param p1
	 * @param c2
	 * @return
	 */
	protected Direction computeDirection(Point p1, Connector c2) {
		Direction[] d2 = c2.getDirections();
		Direction bestDirections = d2[0];
		// Calculate the best points with less distance.
		double best = Double.MAX_VALUE;
		for (int j = 0; j < d2.length; j++) {
			double actual = p1.distance(c2.getConnectionPoint(d2[j]));
			if( actual < best ){
				best = actual;
				bestDirections= d2[j];
			}
		}
		return bestDirections;
	}
}
